﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sio.Mdm.Types;
using System.Reflection;
using Sio.Mdm.Server;
using Sio.Mdm.Client;

namespace Sio.Mdm.Entities
{
    partial class EntityFactory
    {   
        public class EntityInfo
        {
            #region Fields
            private Type EntityType;
            public object[] Attributes;
            private EntityAttribute ServiceAttribute;
            private Dictionary<string, List<ValidationAttribute>> Validation;
            private static readonly List<ValidationAttribute> EmptyValidators = new List<ValidationAttribute>();
            #endregion

            #region Construction
            public EntityInfo(Type entityType)
            {
                EntityType = entityType;
                Attributes = entityType.GetCustomAttributes(true);
                ServiceAttribute = GetAttribute<EntityAttribute>();
            }
            #endregion

            #region Properties

            #region Public

            #region ServiceType
            /// <summary>
            /// Get service implementing interface for entity
            /// </summary>
            public Type ServiceType
            {
                get
                {
                    return ServiceAttribute == null ? typeof(IService) : ServiceAttribute.InterfaceType;
                }
            }
            #endregion

            #region CreateMethod
            public string CreateMethod
            {
                get
                {
                    return ServiceAttribute == null ? "Create" : ServiceAttribute.CreateMethod;
                }
            }
            #endregion

            #region SelectMethod
            public string SelectMethod
            {
                get
                {
                    return ServiceAttribute == null ? "Select" : ServiceAttribute.SelectMethod;
                }
            }
            #endregion

            #region DeleteMethod
            public string DeleteMethod
            {
                get
                {
                    return ServiceAttribute == null ? "Delete" : ServiceAttribute.DeleteMethod;
                }
            }
            #endregion

            #region UpdateMethod
            public string UpdateMethod
            {
                get
                {
                    return ServiceAttribute == null ? "Update" : ServiceAttribute.UpdateMethod;
                }
            }
            #endregion

            #region PrimaryKey
            private PrimaryKeyAttribute _primaryKey;
            public PrimaryKeyAttribute PrimaryKey
            {
                get
                {
                    if (_primaryKey == null)
                    {
                        _fields = new Dictionary<string, FieldAttribute>();
                        ParseFields();
                    }

                    return _primaryKey;
                }
            }
            #endregion


            #region DeletedStatus
            private DeletedStatusAttribute _deletedStatusField;
            private bool _deletedStatusFieldParsed = false;
            public DeletedStatusAttribute DeletedStatusField
            {
                get
                {
                    if ((_deletedStatusField == null) || (!_deletedStatusFieldParsed))
                    {
                        _fields = new Dictionary<string, FieldAttribute>();
                        ParseFields();
                        _deletedStatusFieldParsed = true;
                    }

                    return _deletedStatusField;
                }
            }
            #endregion

            #region Fields, Methods
            private Dictionary<string, FieldAttribute> _fields;
            public Dictionary<string, FieldAttribute> Fields
            {
                get
                {   
                    if (null == _fields)
                    {
                        _fields = new Dictionary<string, FieldAttribute>();
                        ParseFields();
                    }
                    return _fields;
                }
            }
            private Dictionary<string, MethodAttribute> _methods;
            public Dictionary<string, MethodAttribute> Methods
            {
                get
                {
                    if (null == _methods)
                    {
                        _methods = new Dictionary<string, MethodAttribute>();
                        ParseMethods();
                    }

                    return _methods;
                }
            }
            #endregion

            #region TableName
            /// <summary>
            /// Get database table name of entity
            /// </summary>
            public string TableName
            {
                get
                {
                    return ServiceAttribute.TableName;
                }
            }
            #endregion

            #region DmlTableName
            /// <summary>
            /// Get database table name for data manipulation statement (update, insert, delete) of entity
            /// </summary>
            public string DmlTableName
            {
                get
                {
                    return ServiceAttribute.DmlTableName;
                }
            }
            #endregion

            #region FiledMapping
            private Dictionary<string, string> _dbMapping;
            public Dictionary<string, string> FieldMapping
            {
                get
                {
                    if (null == _dbMapping)
                    {
                        _dbMapping = new Dictionary<string, string>();
                    }

                    return _dbMapping;
                }
            }
            #endregion 

            #region IsSettingsEntity
            /// <summary>
            /// Special case for settings attribute
            /// </summary>
            public bool IsSettingsEntity
            {
                get
                {
                    return ServiceAttribute is EntitySettingsAttribute;
                }
            } 
            #endregion

            #endregion
            
            #endregion

            #region Methods

            #region Public
            public List<string> GetSelectableFields()
            {
                List<string> result = new List<string>();

                foreach (KeyValuePair<string, FieldAttribute> pair in Fields)
                {
                    if (pair.Value.SelectMethod == SelectMethod ||
                        pair.Value is SelfReferenceFieldAttribute)
                    {
                        result.Add(pair.Value.Name.ToUpper());
                    }
                }

                return result;
            }
            public List<string> GetInsertableFields()
            {
                List<string> result = new List<string>();

                foreach (KeyValuePair<string, FieldAttribute> pair in Fields)
                {
                    if (pair.Value.SelectMethod == SelectMethod)
                    {
                        if (pair.Value.Name == PrimaryKey.Name && PrimaryKey.IsIdentity)
                        {
                            continue;
                        }

                        result.Add(pair.Value.Name.ToUpper());
                    }
                }

                return result;
            }
            public List<string> GetUpdatableFields(IEntity entity)
            {
                List<string> result = new List<string>();

                foreach (KeyValuePair<string, FieldAttribute> pair in Fields)
                {
                    if (pair.Value.UpdateMethod == UpdateMethod)
                    {
                        if (pair.Value.Name == PrimaryKey.Name)
                        {
                            continue;
                        }

                        string field = FieldMapping[pair.Value.Name.ToLower()];
                        object value = entity.GetField(field);

                        if (value == pair.Value.DefaultValue)
                        {
                            continue;
                        }

                        result.Add(pair.Value.Name.ToUpper());
                    }
                }

                return result;
            }
            public IList<ValidationAttribute> GetValidators(string fieldName)
            {
                return Validation.ContainsKey(fieldName) ?
                    Validation[fieldName].AsReadOnly() : EmptyValidators.AsReadOnly();
            }
            public object GetFieldDefaultValue(string fieldName)
            {
                FieldAttribute attr = Fields[fieldName.ToLower()];
                return attr.DefaultValue;
            }
            #endregion

            #region Internal
            internal Result Create(IEntity entity)
            {
                return InvokeMethod(CreateMethod, entity, Session.Current);
            }
            internal Result Delete(IEntity entity)
            {
                return InvokeMethod(DeleteMethod, entity, Session.Current);
            }
            internal Result Update(IEntity entity)
            {
                EntityCollection col =
                    Activator.CreateInstance(typeof(EntityCollection<>).MakeGenericType(EntityType))
                    as EntityCollection;

                col.Add(entity);

                Result error = Update(ref col);

                if (error == null)
                {       
                    IEntity other = col[0] as IEntity;

                    foreach (string fieldName in entity.FieldsName)
                    {
                        string field = FieldMapping[fieldName.ToLower()];
                        entity.SetField(field, other.GetField(field));                            
                    }
                }

                return error;
            }
            internal Result Update(ref EntityCollection col)
            {
                object[] args = new object[] { col, Session.Current };
                Result error = InvokeMethod(
                    "Update",
                    Type.EmptyTypes,
                    new Type[] { col.GetType().MakeByRefType(), typeof(Session) },
                    ref args
                );

                //if (error == null)
                //{
                //    //col.Clear();
                //    EntityCollection other = args[0] as EntityCollection;

                //    foreach (IEntity entity in other)
                //    {
                //        col.Add(entity);
                //    }
                //}

                return error;
            }
            internal Result Select(IEntity entity, ISioFilter criteria)
            {
                EntityCollection col = 
                    Activator.CreateInstance(typeof(EntityCollection<>).MakeGenericType(EntityType))
                    as EntityCollection;
                
                object[] args = new object[] { col, criteria, Session.Current };
                Result error = InvokeMethod(
                    "Select",
                    Type.EmptyTypes,
                    new Type[] { col.GetType().MakeByRefType(), typeof(ISioFilter<>), typeof(Session) },
                    ref args
                );

                if (error == null)
                {
                    col = args[0] as EntityCollection;

                    if (col.Count == 0)
                    {
                        error = new EntityNotFoundResult();
                    }
                    else
                    {
                        IEntity other = col[0] as IEntity;

                        foreach (string fieldName in entity.FieldsName)
                        {
                            string field = FieldMapping[fieldName.ToLower()];
                            entity.SetField(field, other.GetField(field));
                        }
                    }
                }

                return error;
            }

            internal Result InvokeMethod(string name, params object[] arguments)
            {
                IService service = GetEntityService(ServiceAttribute);                
                Result error = null;
                var method = GetMethodForInvokeEntity(name);

                try
                {
                    return method.Invoke(service, arguments) as Result;
                }
                catch (Exception ex)
                {
                    error = new Result("Unable to invoke buissnes logic. method {0} for {1}", ex);
                    error.Function = String.Format("{0}.{1}", ServiceAttribute.InterfaceType.Name, name);
                }

                return error;
            }
            internal Result InvokeMethod(string name, Type[] genericParams, Type[] parameterTypes, ref object[] arguments)
            {
                IService service = GetEntityService(ServiceAttribute);                
                Result error = null;

                var method = GetMethodForInvokeCollection(name, genericParams, parameterTypes);

                try
                {
                    if (method.IsGenericMethod)
                    {
                        method = method.MakeGenericMethod(genericParams);
                    }

                    return method.Invoke(service, arguments) as Result;
                }
                catch (Exception ex)
                {
                    error = new Result("Unable to invoke buissnes logic. method {0} for {1}", ex);
                    error.Function = String.Format("{0}.{1}", ServiceAttribute.InterfaceType.Name, name);
                }

                return error;
            }
            internal Result Validate(IEntity entity, string fieldName, object value)
            {
                return null;
            }
            internal object InvokeSettingGet(string key)
            {
                //bymatejg
                key = string.Format("{0}.{1}", EntityType.Name, key);
                //end bymatejg

                object value = null;
                object[] args = new object[] { key, value, SessionFactory.Session };
                Type[] argTypes = new Type[] { typeof(string), typeof(object).MakeByRefType(), typeof(Session)};
                
                Result result = ServiceFactory.InvokeServiceMethodExact(
                    "SettingsService",
                    "GetSetting",
                    argTypes,
                    Type.EmptyTypes,
                    args
                ) as Result;

                if (result == null)
                {
                    return args[1] == null ? GetFieldDefaultValue(key) : args[1];
                }

                return GetFieldDefaultValue(key);
            }
            internal object InvokeSettingSet(string key, object value)
            {   
                //bymatejg
                key = string.Format("{0}.{1}", EntityType.Name, key);
                //end bymatejg

                object[] args = new object[] { key, value, SessionFactory.Session };
                Type[] argTypes = new Type[] { typeof(string), typeof(object), typeof(Session) };

                Result result = ServiceFactory.InvokeServiceMethodExact(
                    "SettingsService",
                    "SetSetting",
                    argTypes,
                    Type.EmptyTypes,
                    args
                ) as Result;
                
                return null;
            }
            #endregion

            #region Private

            #region GetAttribute
            private T GetAttribute<T>() where T : Attribute
            {
                return GetAttribute<T>(Attributes);
            }
            internal static T GetAttribute<T>(object[] attributes) where T : Attribute
            {
                foreach (object attr in attributes)
                {
                    if (attr is T)
                    {
                        return (T)attr;
                    }
                }

                return null;
            }
            #endregion

            #region ParseFields
            private void ParseFields()
            {
                if (_fields.Count > 0)
                {
                    return;
                }

                List<PropertyInfo> properties = FlattenProperties(EntityType);

                foreach (PropertyInfo prop in properties)
                {
                    object[] attributes = prop.GetCustomAttributes(true);
                    FieldAttribute attr = GetAttribute<FieldAttribute>(attributes);

                    ParseValidationAttributes(prop, attributes);

                    if (attr == null)
                    {   
                        continue;
                    }

                    if (_fields.ContainsKey(prop.Name.ToLower()))  //by matejg
                    {
                        // TODO: Check wheater we should merge or throw excpetion
                        // We will not support same fields on different entities
                        continue;
                    }

                    if (String.IsNullOrEmpty(attr.Name))
                    {
                        attr.Name = prop.Name;
                    }

                    FieldMapping[attr.Name.ToLower()] = prop.Name;

                    // TODO: Explicitly check for type
                    attr.AllowNull = prop.PropertyType.Name.StartsWith("Nullable");
                    attr.FieldType = prop.PropertyType;

                    _fields.Add(prop.Name.ToLower(), attr);

                    if (typeof(PrimaryKeyAttribute).IsAssignableFrom(attr.GetType()))
                    {
                        _primaryKey = attr as PrimaryKeyAttribute;
                    }

                    if (typeof(DeletedStatusAttribute).IsAssignableFrom(attr.GetType()))
                    {
                        _deletedStatusField = attr as DeletedStatusAttribute;
                    }

                    if (prop.PropertyType.IsValueType && attr.DefaultValue == null)
                    {
                        attr.DefaultValue = Activator.CreateInstance(prop.PropertyType);
                    }
                }
            }
            #endregion

            #region ParseMethods
            private void ParseMethods()
            {
                List<MethodInfo> methods = FlattenMethods(EntityType);

                foreach (MethodInfo m in methods)
                {
                    object[] attributes = m.GetCustomAttributes(true);
                    MethodAttribute attrib = GetAttribute<MethodAttribute>(attributes);

                    if (null == attrib)
                    {
                        continue;
                    }

                    if (String.IsNullOrEmpty(attrib.MethodName))
                    {
                        attrib.MethodName = m.Name;
                    }

                    if (String.IsNullOrEmpty(attrib.ServiceName))
                    {
                        attrib.ServiceType = ServiceType;
                    }

                    attrib.EntityType = EntityType;
                    _methods.Add(m.Name, attrib);
                }
            } 
            #endregion

            #region ParseValidationAttributes
            private void ParseValidationAttributes(PropertyInfo prop, object[] attributes)
            {
                Validation = new Dictionary<string, List<ValidationAttribute>>();

                foreach (Attribute attr in attributes)
                {
                    if (attr is ValidationAttribute)
                    {
                        if (!Validation.ContainsKey(prop.Name))
                        {
                            Validation.Add(prop.Name, new List<ValidationAttribute>());
                        }

                        Validation[prop.Name].Add(attr as ValidationAttribute);
                    }
                }
            }
            #endregion

            #region FlattenProperties
            /// <summary>
            /// Fins all properties of all fields inherited from all interfaces.
            /// Remeber interface can have multiple interfaces as base
            /// </summary>
            /// <param name="type"></param>
            /// <returns>
            /// List of all properties for passed <paramref name="type"/> including
            /// all inherited interfaces (yeah multi-inheritance is back)
            /// </returns>
            public List<PropertyInfo> FlattenProperties(Type type)
            {
                List<PropertyInfo> list = new List<PropertyInfo>();

                // Add current properties
                list.AddRange(type.GetProperties());

                Type[] interfaces = type.GetInterfaces();

                // Iterate and add all other interfaces
                foreach (Type i in interfaces)
                {
                    if (typeof(IEntity).IsAssignableFrom(i))
                    {
                        list.AddRange(FlattenProperties(i));
                    }
                }

                return list;
            }
            #endregion

            #region FlattenMethods
            private List<MethodInfo> FlattenMethods(Type type)
            {
                return FlattenMethods(type, typeof(IEntity));
            }
            /// <summary>
            /// Flatten methods so we use kind of inheritance on desired type
            /// </summary>
            /// <param name="type"></param>
            /// <returns></returns>
            public static List<MethodInfo> FlattenMethods(Type type, Type filterType)
            {
                List<MethodInfo> list = new List<MethodInfo>();

                list.AddRange(type.GetMethods());

                Type[] interfaces = type.GetInterfaces();

                // Iterate and add all other interfaces
                foreach (Type i in interfaces)
                {
                    if (filterType != null)
                    {
                        if (filterType.IsAssignableFrom(i))
                        {
                            list.AddRange(FlattenMethods(i, filterType));
                        }
                    }
                    else
                    {
                        list.AddRange(FlattenMethods(type, filterType));
                    }
                }

                return list;
            }
            #endregion

            #region GetMethodForInvoke
            private MethodInfo GetMethodForInvokeEntity(string name)
            {
                List<MethodInfo> allMethods = FlattenMethods(ServiceAttribute.InterfaceType, typeof(IService));
                var methods = allMethods.Where(n => n.Name == name);

                foreach (MethodInfo m in methods)
                {
                    ParameterInfo[] parameters = m.GetParameters();
                    
                    if (parameters[0].ParameterType == EntityType ||
                        parameters[0].ParameterType == EntityType.MakeByRefType())
                    {
                        return m;
                    }
                }

                return null;
            }
            private MethodInfo GetMethodForInvokeCollection(string name, Type[] genericParams, Type[] parameterTypes)
            {
                List<MethodInfo> allMethods = FlattenMethods(ServiceAttribute.InterfaceType, typeof(IService));
                var methods = allMethods.Where(n => n.Name == name);
                Type collectionType = typeof(EntityCollection<>).MakeGenericType(EntityType).MakeByRefType();

                foreach (MethodInfo m in methods)
                {   
                    Type[] generic = m.GetGenericArguments();
                    bool isGenericValid = genericParams.Length == generic.Length;
                    
                    //int i = 0, 
                    //    count = generic.Length < genericParams.Length? generic.Length: genericParams.Length;

                    //for (i = 0; i < count; i++)
                    //{
                    //    if (generic[i] != genericParams[i])
                    //    {
                    //        isGenericValid = false;
                    //        break;
                    //    }
                    //}

                    if (!isGenericValid)
                    {
                        continue;
                    }

                    int i = 0;
                    ParameterInfo[] parameters = m.GetParameters();

                    for (i = 0; i < parameters.Length; i++)
                    {
                        if (parameters[i].ParameterType.IsGenericType)
                        {
                            if (parameters[i].ParameterType.GetGenericArguments().Length !=
                                parameterTypes[i].GetGenericArguments().Length ||
                                parameterTypes[i].GetGenericTypeDefinition() != parameters[i].ParameterType.GetGenericTypeDefinition())
                            {
                                break;
                            }
                        }
                        else if (parameters[i].ParameterType != parameterTypes[i])
                        {
                            break;
                        }
                    }

                    if(i == parameters.Length)
                    {
                        return m;
                    }
                }

                return null;
            } 
            #endregion

            #endregion

            #endregion
        }     
    }
}
